Flutter json_serializable 实现原理

json_serializable 是一个 Dart 包,用于自动化生成代码,实现 JSON 序列化和反序列化。它主要利用注解来标记类,然后通过代码生成的方式自动创建对应的序列化逻辑。

在旧笔记《Flutter JSON Serializable》,梳理 json_serializable 的使用。但是对其底层实现原理挖掘不够彻底,在这篇笔记中,重点分析其底层实现原理。


json_serializable、build_runner 与 source_gen

build_runner 是 Dart 的一个构建系统,用于在 Dart 项目中执行代码生成任务。它不仅用于 json_serializable,还可用于其他需要代码生成的场景,如 protobuf、GraphQL 等。

build_runner 作为构建工具,可以托管多个生成器(generator),并且负责触发并控制生成过程。

source_gen 是一个 Dart 的库,提供了一套 API,让开发者可以更容易地编写代码生成逻辑。它被用作在 Dart 项目中生成 Dart 代码的框架。

在 build_runner 的生态中,source_gen 提供了基本的 Generator 类和其他实用工具,帮助开发者创建自定义的代码生成器。

综合来说:

json_serializable 利用 source_gen 提供的工具和框架来实现具体的代码生成逻辑。这个生成逻辑被包装在一个或多个 Generator 中。然后,build_runner 作为构建系统,调用这些 Generator,负责运行这些代码生成任务,并将生成的代码集成到你的项目中。


source_gen

json_serializable 是一个遵循 Dart Build Runner 生态的扩展库。在 Build Runner 中,支持遵循 source_gen 的协议进行代码生成。

在旧笔记《Flutter JSON Serializable》中,对 source_gen 有所介绍,其核心类为 Generator,核心方法是 generate,签名如下:

FutureOr<String?> generate(
    LibraryReader library, 
    BuildStep buildStep) => null;

GeneratorForAnnotation 是 Generator 的一个派生类,专门匹配源码文件的顶层元素存在注解的情况。

json_serializable 中的具体生成器,都是基于 GeneratorForAnnotation 派生的,具体有这些:

在本文中,将重点介绍 JsonSerializableGenerator 生成器。


JsonSerializable 的设置项

JsonSerializableGenerator 中有一个名为 config 的设置项,类型为 JsonSerializable:

JsonSerializable get config => _settings.config.toJsonSerializable();

里面包含了 JsonSerializableGenerator 所能接受的所有配置。对其梳理如下:

anyMap:布尔类型,默认为 false。如果设置为 true,则不会假设 Map 类型为 Map<String, dynamic>(这是 dart:convert 中 JSON 解码返回的 Map 实例的默认类型)。这将增加代码大小,但允许从其他来源(如 package:yaml)返回的 Map 类型。

checked:布尔类型,默认为 false。如果设置为 true,生成的 fromJson 函数将包括额外的检查以验证类型的正确反序列化。如果在反序列化过程中抛出异常,将抛出 CheckedFromJsonException

constructor:字符串类型,默认为 null。指定在创建 fromJson 函数时要针对的命名构造函数。如果值未设置或为空字符串,则使用默认构造函数。如果 createFactoryfalse,此设置无效。

createFactory:布尔类型,默认为 true。如果为 true,将在生成的部分文件中创建一个私有静态方法 _$ExampleFromJson。可以从源类中添加的工厂构造函数调用此方法。

createFieldMap:布尔类型,默认为 false。如果为 true,将在生成的部分文件中创建一个私有静态常量 _$ExampleJsonMeta。其他代码生成器可以使用此常量来支持诸如 fieldRename 之类的功能。

createJsonKeys:布尔类型,默认为 false。如果为 true,将在生成的部分文件中创建一个私有类常量 _$ExampleJsonKeys。此类将包含每个属性,以 JSON 键作为值,提供一种安全的方式从属性访问 JSON 键。

createPerFieldToJson:布尔类型,默认为 false。如果为 true,将在部分文件中生成一个私有静态抽象类 _$ExamplePerFieldToJson。此抽象类将为每个属性包含一个静态函数,提供一种仅对此属性进行编码而不是整个对象的方法。

createToJson:布尔类型,默认为 true。如果为 true,将创建一个顶级函数,可以从类中引用它。

disallowUnrecognizedKeys:布尔类型,默认为 false。如果为 false,则生成的 FromJson 函数将忽略提供的 JSON Map 中无法识别的键。如果为 true,无法识别的键将导致抛出 UnrecognizedKeysException

explicitToJson:布尔类型,默认为 false。如果为 true,生成的 toJson 方法将显式调用嵌套对象上的 toJson。当使用 dart:convert 中的 JSON 编码支持时,会自动在对象上调用 toJson,因此默认行为(explicitToJson: false)是省略 toJson 调用。

fieldRenameFieldRename 类型,默认为 FieldRename.none。定义将类字段名称转换为 JSON 映射键时的自动命名策略。使用值 FieldRename.none(默认值)时,字段名称无需修改即可使用。有关其他选项的详细信息,请参阅 FieldRename。注意:对于使用 JsonKey 注解的字段,JsonKey.name 的值优先于此选项。

genericArgumentFactories:布尔类型,默认为 false。当在具有类型参数(泛型类型)的类上为 true 时,将为 fromJson 和/或 toJson 生成额外的"帮助器"参数,以支持序列化这些类型的值。

ignoreUnannotated:布尔类型,默认为 false。当为 true 时,只有使用 JsonKey 注解的字段才会生成代码。这将与将这些字段注解为 JsonKey.includeToJsonJsonKey.includeFromJson 设置为 false 具有相同的效果。

includeIfNull:布尔类型,默认为 true。决定生成器是否应在序列化输出中包含值为 null 的字段。如果为 true(默认值),则所有字段都将写入 JSON,即使它们为 null。如果字段使用 JsonKey 注解,并且 includeIfNull 的值不为 null,则该值优先。

convertersJsonConverter 列表类型,默认为 null。要应用于此类的 JsonConverter 列表。这允许在多个类上重用自定义的 JsonSerializable


GeneratorHelper

GeneratorHelper 的作用是根据提供的配置和注解,为指定的类生成 JSON 序列化和反序列化的相关代码。它会分析类的字段,根据字段的属性和注解生成对应的代码,如工厂方法、字段映射、JSON 键常量、toJson 方法等。生成的代码通过迭代器的方式返回,可以被进一步处理和输出。

在 JsonSerializableGenerator 中,具体的生成逻辑交由 GeneratorHelper 负责:

final helper = GeneratorHelper(_settings, element, annotation);
return helper.generate();

GeneratorHelper 的整体流程具体分为两个阶段:

  1. 初始化阶段
  2. 生成阶段

初始化阶段主要对应构造方法,接收 Settings、ClassElement 和 ConstantReader 作为参数,并将它们存储在实例变量中。以及将传入的配置与默认配置进行合并。具体代码如下:

class GeneratorHelper extends HelperCore with EncodeHelper, DecodeHelper {
  final Settings _generator;
  final _addedMembers = <String>{};

  GeneratorHelper(
    this._generator,
    ClassElement element,
    ConstantReader annotation,
  ) : super(
            element,
            mergeConfig(
              _generator.config,
              annotation,
              classElement: element,
            ));

第二个阶段是生成阶段,也是最核心部分:

Iterable<String> generate() sync* {
  assert(_addedMembers.isEmpty);

  // 创建一个排序后的字段集合 sortedFields,包含当前类的所有字段。
  final sortedFields = createSortedFieldSet(element);

  // Used to keep track of why a field is ignored. Useful for providing
  // helpful errors when generating constructor calls that try to use one of
  // these fields.
  // 用于跟踪字段被忽略的原因。
  final unavailableReasons = <String, String>{};

  // 遍历 sortedFields,根据字段的可访问性、JsonKey 注解等条件
  // 将字段分为可访问字段和不可访问字段,并记录不可访问字段的原因。
  final accessibleFields = sortedFields.fold<Map<String, FieldElement>>(
    <String, FieldElement>{},
    (map, field) {
      final jsonKey = jsonKeyFor(field);
      if (!field.isPublic && !jsonKey.explicitYesFromJson) {
        unavailableReasons[field.name] = 'It is assigned to a private field.';
      } else if (field.getter == null) {
        assert(field.setter != null);
        unavailableReasons[field.name] =
            'Setter-only properties are not supported.';
        log.warning('Setters are ignored: ${element.name}.${field.name}');
      } else if (jsonKey.explicitNoFromJson) {
        unavailableReasons[field.name] =
            'It is assigned to a field not meant to be used in fromJson.';
      } else {
        assert(!map.containsKey(field.name));
        map[field.name] = field;
      }

      return map;
    },
  );

  var accessibleFieldSet = accessibleFields.values.toSet();

  // 如果启用了 createFactory,则调用 createFactory 方法生成工厂方法
  // 并根据使用情况更新可访问字段集合。
  if (config.createFactory) {
    final createResult = createFactory(accessibleFields, unavailableReasons);
    yield createResult.output;

    final fieldsToUse = accessibleFields.entries
        .where((e) => createResult.usedFields.contains(e.key))
        .map((e) => e.value)
        .toList();

    // Need to add candidates BACK even if they are not used in the factory if
    // they are forced to be used for toJSON
    for (var candidate in sortedFields.where((element) =>
        jsonKeyFor(element).explicitYesToJson &&
        !fieldsToUse.contains(element))) {
      fieldsToUse.add(candidate);
    }

    // Need the fields to maintain the original source ordering
    fieldsToUse.sort(
        (a, b) => sortedFields.indexOf(a).compareTo(sortedFields.indexOf(b)));

    accessibleFieldSet = fieldsToUse.toSet();
  }

  // 从可访问字段集合中移除显式标记为不生成 toJson 的字段。
  accessibleFieldSet
    ..removeWhere(
      (element) => jsonKeyFor(element).explicitNoToJson,
    )

    // Check for duplicate JSON keys due to colliding annotations.
    // We do this now, since we have a final field list after any pruning done
    // by `_writeCtor`.
    // 检查可访问字段的 JSON 键是否有重复,如果有重复则抛出错误。
    ..fold(
      <String>{},
      (Set<String> set, fe) {
        final jsonKey = nameAccess(fe);
        if (!set.add(jsonKey)) {
          throw InvalidGenerationSourceError(
            'More than one field has the JSON key for name "$jsonKey".',
            element: fe,
          );
        }
        return set;
      },
    );

  // 如果启用了 createFieldMap,则调用 createFieldMap 方法生成字段映射。
  if (config.createFieldMap) {
    yield createFieldMap(accessibleFieldSet);
  }

  // 如果启用了 createJsonKeys,则调用 createJsonKeys 方法生成 JSON 键常量。
  if (config.createJsonKeys) {
    yield createJsonKeys(accessibleFieldSet);
  }

  // 如果启用了 createPerFieldToJson,则调用 createPerFieldToJson 
  // 方法生成每个字段的 toJson 方法。
  if (config.createPerFieldToJson) {
    yield createPerFieldToJson(accessibleFieldSet);
  }

  // 如果启用了 createToJson,则调用 createToJson 方法生成整个对象的 toJson 方法。
  if (config.createToJson) {
    yield* createToJson(accessibleFieldSet);
  }

  // 最后,将生成的所有成员内容作为迭代器返回。
  yield* _addedMembers;
}

其中,还包含两个辅助方法:

其中 generate 方法使用了 Dart 中的同步生成器(Synchronous Generator)特性。同步生成器允许你定义一个函数,该函数可以在每次迭代时生成(yield)一系列值,而不是立即返回所有值。

generate 方法中,关键字 sync* 表示这是一个同步生成器函数。函数体内部使用 yield 关键字来生成值。每次调用 yield,函数的执行都会暂停,并将生成的值返回给迭代器。当迭代器再次请求值时,函数会从上次暂停的位置恢复执行,直到遇到下一个 yield 或函数结束。

如何理解呢?返回类型是 Iterable<String> 是一个可迭代的,外部将以迭代方式访问 generate 方法,而 generate 方法每次则走一个 yield

现在,让我们看看 generate 方法的具体流程:

  1. 首先,它创建一个排序后的字段集合 sortedFields,包含当前类的所有字段。

  2. 然后,它遍历 sortedFields,根据字段的可访问性、JsonKey 注解等条件,将字段分为可访问字段和不可访问字段,并记录不可访问字段的原因。

  3. 如果启用了 createFactory,它会调用 createFactory 方法生成工厂方法,并使用 yield 将生成的代码作为迭代器的第一个值返回。

  4. 接下来,它根据一些条件(如 createFieldMapcreateJsonKeyscreatePerFieldToJsoncreateToJson 等)调用相应的方法生成其他代码片段,并使用 yield 将生成的代码作为迭代器的后续值返回。

  5. 最后,它使用 yield*_addedMembers 集合中的所有成员内容作为迭代器的最后一部分返回。

通过使用迭代器,GeneratorHelper 可以将代码生成过程分解为多个步骤,每个步骤生成一部分代码。这样做的好处是可以更好地组织和控制代码生成过程,并且可以根据需要选择性地生成某些部分的代码。

最终,通过迭代 generate 方法返回的迭代器,可以获取所有生成的代码片段,并将它们组合在一起形成完整的生成代码。


HelperCore

单独看 GeneratorHelper 还是有点云里雾里,原因是还有一部分工作放在 GeneratorHelper 的基类 HelperCore 中。

首先,HelperCore 中包含一系列方法和属性:

HelperCore 中的这些方法和属性为具体的生成器类提供了基础设施,使得它们可以更加专注于特定的代码生成逻辑,而无需关注一些通用的辅助功能。通过继承 HelperCore,具体的生成器类可以复用这些辅助方法和属性,提高代码的可读性和可维护性。


DecodeHelper

GeneratorHelper 同时还 mixin 了 EncodeHelperDecodeHelper,顾名思义,他们分别负责序列化和反序列化。再本节中梳理 DecodeHelper,它专门负责处理反序列化逻辑,即将 JSON 格式的数据转换回 Dart 对象。这一过程涉及读取 JSON 数据,并根据类定义及其注解生成 Dart 类的实例。

反序列化的核心在于如何将 JSON 对象的字段映射到 Dart 类的构造函数或属性上。DecodeHelper 提供了一系列方法和逻辑,以确保这一映射过程准确无误。

核心方法是 _deserializeForField,作用是为给定的字段生成反序列化代码。具体逻辑如下:

获取字段的 JSON 键名 jsonKeyName,使用 safeNameAccess 方法对键名进行转义,以确保其安全性。

final jsonKeyName = safeNameAccess(field);

获取目标类型 targetType,如果提供了构造函数参数 ctorParam,则使用参数的类型,否则使用字段的类型。

final targetType = ctorParam?.type ?? field.type;

获取字段的上下文助手 contextHelper,通过调用 getHelperContext 方法。

final contextHelper = getHelperContext(field);

获取默认值,以及读取值的方法:

获取字段的 JsonKey 注解信息 jsonKey,包括默认值 defaultValue 和自定义读取函数名 readValueFunc

final jsonKey = jsonKeyFor(field);
final defaultValue = jsonKey.defaultValue;
final readValueFunc = jsonKey.readValueFunctionName;

定义 deserialize 函数,用于生成反序列化表达式。它使用 contextHelper.deserialize 方法,传入目标类型、表达式和默认值,并返回生成的反序列化代码。

String deserialize(String expression) => contextHelper
    .deserialize(
      targetType,
      expression,
      defaultValue: defaultValue,
    )
    .toString();

注意,这是一个位于定义在函数里的函数。

根据配置和参数生成反序列化的值表达式 value

处理异常情况:如果反序列化过程中抛出 UnsupportedTypeError 异常,则使用 createInvalidGenerationError 方法创建一个错误,并抛出该错误,指示无法生成反序列化代码。

String value;
try {
  if (config.checked) {
    value = deserialize('v');
    if (!checkedProperty) {
      final readValueBit =
          readValueFunc == null ? '' : ',readValue: $readValueFunc,';
      value = '\$checkedConvert($jsonKeyName, (v) => $value$readValueBit)';
    }
  } else {
    //...
    value = deserialize(
      readValueFunc == null
          ? 'json[$jsonKeyName]'
          : '$readValueFunc(json, $jsonKeyName)',
    );
  }
} on UnsupportedTypeError catch (e) // ignore: avoid_catching_errors
{
  throw createInvalidGenerationError('fromJson', field, e);
}

如果存在默认值 defaultValue,则进行警告处理:

if (defaultValue != null) {
  if (jsonKey.disallowNullValue && jsonKey.required) {
    log.warning('The `defaultValue` on field `${field.name}` will have no '
        'effect because both `disallowNullValue` and `required` are set to '
        '`true`.');
  }
}

返回生成的反序列化值表达式 value

return value;

_UnifiedGenerator

_UnifiedGenerator 是一个组合生成器,它将多个 GeneratorForAnnotation 实例集成为一个单一的生成器。其主要目的是在使用 @JsonEnum@JsonSerializable 注解时,合并重复项,以避免生成重复的枚举辅助函数。这样做可以确保输出的生成代码不会因为多个注解而重复相同的功能,提高了代码的整洁度和效率。

具体到实现细节,_UnifiedGenerator 包含了一个 _generators 的列表,这个列表存放了所有的 GeneratorForAnnotation 实例。在其 generate 方法中,这个类遍历每一个生成器,并对库中所有有注解的元素调用对应的生成器方法。通过这种方式,_UnifiedGenerator 收集所有生成器产生的代码片段,将它们存入一个集合中以去除重复,最后将这些片段合并成一个字符串返回。

这种设计允许每个生成器专注于其特定的序列化逻辑,而 _UnifiedGenerator 负责协调这些生成器的输出,确保最终生成的代码既全面又不冗余。


如何调试 json_serializable?

json_serializable 是一个基于 build_runner 的扩展库,在理解代码的时候,有一点很不方便的地方,是我不知道该如何去 Debug。如果能够 Debug,阅读代码的效率将会高很多。

目前,我只能通过非常原始的 print 日志打印方式,来查看逻辑,效率很低。

我注意到 json_serializable 的单测用例集很丰富。json_serializable 的测试部分,应该会有一个不依赖 build_runner 的框架,这样我就可以在单测中插入断点了。

如果你有更好的方法,欢迎留言交流!


本文作者:Maeiee

本文链接:Flutter json_serializable 实现原理

版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!


喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!